/**
 * Class: PrecisionClock
 *
 * Purpose: 
 *
 * Created: Feb 1, 2010 @ 2:35:42 PM
 * @author Ken Scott
 */
package org.openfantasia.util.time;

import org.openfantasia.util.Logger;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class PrecisionClock implements Runnable {

	private double period;

	private ScheduledExecutorService exec;

	private long startTime;

	private long lastTime = 0L;

	private long tickCount = 0L;

	private List<PrecisionClockListener> listeners = new ArrayList<PrecisionClockListener>();


	public PrecisionClock() {
	}


	public synchronized void start(double period) {
		if (this.exec != null) {
			throw new IllegalStateException("PrecisionClock is already started");
		}

		this.period = period;
		exec = Executors.newSingleThreadScheduledExecutor();
		this.startTime = System.nanoTime();
		this.lastTime = this.startTime;
		this.tickCount = 0;
		exec.scheduleAtFixedRate(this, 0, (long) this.period, TimeUnit.SECONDS);
	}


	public final void run() {
		long now = System.nanoTime();
		long delay = now - lastTime;
		lastTime = now;
		for (PrecisionClockListener listener : listeners) {
			listener.tick(tickCount, now - startTime, delay);
		}
		tickCount++;
	}


	public synchronized void stop() {
		if (exec != null) {
			exec.shutdownNow();
			exec = null;
		}
	}


	public void addListener(PrecisionClockListener listener) {
		if (!listeners.contains(listener)) {
			listeners.add(listener);
		}
	}


	public void removeListener(PrecisionClockListener listener) {
		listeners.remove(listener);
	}


	public double getPeriod() {
		return period;
	}


	public double getPeriodUS() {
		return (double) TimeUnit.MICROSECONDS.convert((long) period, TimeUnit.SECONDS);
	}


	public double getPeriodMS() {
		return (double) TimeUnit.MILLISECONDS.convert((long) period, TimeUnit.SECONDS);
	}


	public double getPeriodNS() {
		return (double) TimeUnit.NANOSECONDS.convert((long) period, TimeUnit.SECONDS);
	}


	public static void main(String[] args) {
		final PrecisionClock clock = new PrecisionClock();
		clock.addListener(new PrecisionClockListener() {

			public void tick(long tickCount, long currentTimeNanos, long cycleTimeNanos) {
				Logger.log("Tick: " + tickCount + "; period=" + clock.getPeriod() + "; time=" + currentTimeNanos + "; cycle=" + cycleTimeNanos + "; average=" + ((double) currentTimeNanos / (double) tickCount));
			}
		});
		clock.start(1d);
	}
}
